セキュアな通信のCloud Run Functionsを作成したいのでDirect VPC Egress(プレビュー)を試してみた #cm_google_cloud_adcal_2024

セキュアな通信のCloud Run Functionsを作成したいのでDirect VPC Egress(プレビュー)を試してみた #cm_google_cloud_adcal_2024

Clock Icon2024.12.02

データ事業本部のsutoです。
この記事はクラスメソッドの Google Cloud アドベントカレンダー2024 の2日目の記事です。

Cloud Run Functionsの外部アクセスを制限し、VPC経由によるプライベート通信経路にしたいという要件はよくあるかと思います。

Google Cloudでこれを実現するための方法がいくつかありますが、今回は__Direct VPC Egress__を利用する方法を検証してみたいと思います。

実装したい構成

今回の検証で実装したいことは以下となります。

  • 検証用Cloud Run Functionsを作成し、Google Storage APIのアクセス可能か確認、またインターネットアクセスを確認する処理を記述する
  • 作成したCloud Run Functionsが、インターネットアクセスできないように構成する
  • 作成したCloud Run Functionsが、VPC経由でGoogle APIにのみアクセスできるように構成する

スクリーンショット 2024-11-18 112658

Direct VPC Egress

これまでの Cloud Run では、VPC 内のリソースやパブリック IP を使用しない Cloud SQL、Memorystore などのリソースに対して接続する場合、サーバーレス VPC アクセスコネクタ のインスタンスを介して VPC に接続する必要がありました。

https://dev.classmethod.jp/articles/cloud-functions-enhanced-security/

ただ、Cloud Run で Direct VPC Egress を有効にすると、サーバーレス VPC アクセスコネクタを使用せずに VPC ネットワークにトラフィックを送信できるようになり、機能自体はすでにGAされています。

しかし、GAとなっているのはClou Run ServiceやCloud Run Jobsに対してのみで、本件で検証するCloud Run Functionsにおいては「2024年11月15日時点でプレビュー」の状態ですのでご注意ください。

限定公開のGoogleアクセス

Cloud Run Functions → VPC の通信はDirect VPC Egressで実装しますが、VPC → Google APIの通信は「限定公開のGoogleアクセス」を実装することで実現します。

限定公開のGoogleアクセスの概要は以下の記事が非常にわかりやすいです。

https://blog.g-gen.co.jp/entry/private-google-access-explained

今回は、「VPC Service Controls でサポートされている Google API にだけアクセス可能」というより限定したかたちでやってみようと思うので、Cloud DNSも使って「restricted.googleapis.com」で設定します。

やってみた

検証用のサブネットと関数を作成

まず関数のトラフィック用のサブネット(test-functions-subnet)を作成します。
(VPCのファイアウォール ルールには外部IPの許可はしていないものとなります)

  • サブネットでは「限定公開のGoogleアクセス」をオンに設定

スクリーンショット 2024-11-14 141057

次にCloud Run関数を作成します。

  • タブ「コンテナ」では関数内で使用する環境変数を追加
  • タブ「ネットワーキング」で「アウトバウンドトラフィックを送信する」にチェックし、「VPCに直接トラフィックを送信する」にすることでDirect VPC Egressを設定
  • 「すべてのトラフィックをVPCにルーティング」を選択することで、すべてのリクエストはVPCのプライベート経由の通信とする

スクリーンショット 2024-11-14 141801

スクリーンショット 2024-11-14 142342

スクリーンショット 2024-11-14 142356

スクリーンショット 2024-11-18 155538

スクリーンショット 2024-11-14 142411

関数のソースコード

関数のソースコードには、pythonを用いて以下のコードを記述しました。

main.pyを表示するにはここをクリック
from flask import jsonify, Request
import functions_framework
from google.cloud import logging, storage
import os
from retrying import retry
import requests
from logging import DEBUG, INFO, getLogger

logging_client = logging.Client()
logging_client.setup_logging()
logger = getLogger(__name__)
logger.setLevel(DEBUG)

@retry(stop_max_attempt_number=3, wait_exponential_multiplier=1000)
def list_files_from_bucket(bucket_name):
    try:
        storage_client = storage.Client()
        blobs = storage_client.list_blobs(bucket_name)
        page_token = None
        file_list = []
        while True:
            blobs = storage_client.list_blobs(bucket_name, page_token=page_token, max_results=10000)
            file_list.extend(blob.name for blob in blobs if not blob.name.endswith('/'))
            page_token = blobs.next_page_token
            logger.debug(f"page_token:{page_token}")
            if page_token is None:
                break
        if not file_list:
            logger.warning(f"No files found in bucket {bucket_name}")
            raise ValueError(f"No files found in bucket {bucket_name}")
        return file_list
    except Exception as e:
        logger.error(f"Error listing files from bucket {bucket_name}: {e}")
        raise

@retry(stop_max_attempt_number=3, wait_exponential_multiplier=1000)
def check_internet(request_url):
    try:
        # Googleにリクエストを送信して接続を確認
        response = requests.get(request_url, timeout=5)
        response.raise_for_status()  # HTTPエラーを例外として発生させる
        # ステータスコードが200の場合、インターネット接続が可能と判断
        return jsonify({"status": "success", "message": "Internet connection is available."}), 200
    except requests.ConnectionError:
        error_message = "No internet connection."
        return error_message
    except requests.Timeout:
        error_message = "Connection timed out."
        return error_message
    except requests.exceptions.HTTPError as e:
        error_message = "HTTP error occurred."
        return error_message
    except Exception as e:
        error_message = "An unexpected error occurred."
        return error_message

@functions_framework.http
def test_gcs_get(request: Request):
    """HTTPリクエストを処理して指定バケット配下のファイル名を返す、インターネット接続のレスポンスを返すCloud Function"""
    try:
        URL= os.getenv("URL")
        SRC_BUCKET = os.getenv("SRC_BUCKET")
        if not SRC_BUCKET:
            raise ValueError("SRC_BUCKET environment variable is not set")
        if not URL:
            raise ValueError("URL environment variable is not set")
        file_names = list_files_from_bucket(SRC_BUCKET)
        check_result = check_internet(URL)
        return jsonify({"file_names": file_names, "bucket": SRC_BUCKET, "ckeck_connection": check_result}), 200
    except ValueError as ve:
        logger.exception(f"Value error: {ve}")
        return jsonify({"error": str(ve)}), 599
    except Exception as e:
        logger.exception(f"Unexpected error: {e}")
        return jsonify({"error": "Internal Server Error"}), 599
requirements.txtを表示するにはここをクリック
    functions-framework==3.*
    google-cloud-storage>=1.42.0
    google-cloud-logging>=3.0.0
    retrying>=1.3.3

追記
Cloud Functionsで作成した既存の関数であっても、Cloud Run画面の方で上記のように編集を行えばDirect VPC Egressの設定が可能です。(Cloud Functionsの画面ではDirect VPC Egressの設定はできません)

限定公開のGoogleアクセスのドメインオプション設定

次にrestricted.googleapis.comを使ったドメイン設定を行います。
今回はルーティングやファイアウォール設定がデフォルトのサブネットで検証していますが、既存のVPCやサブネットを利用する場合は以下を確認してください。

  • 199.36.153.4/30 が デフォルトのインターネットゲートウェイへ向いている ことを確認
  • VPC ファイアウォールで 199.36.153.4/30 への 443/TCP の 下り (Egress) 通信が拒否されていない ことを確認 (デフォルト状態では許可)

Cloud DNSを開いて「googleapis.com」という限定公開 DNS ゾーンを作成します。
次に作成したゾーンに以下を追加します。

  • DNS名: restricted.googleapis.com
    • タイプ: A
    • IPv4アドレス:199.36.153.4, 199.36.153.5, 199.36.153.6, 199.36.153.7
  • DNS名: *.googleapis.com
    • タイプ: CNAME
    • 正規名: restricted.googleapis.com

スクリーンショット 2024-11-14 154934

スクリーンショット 2024-11-14 154944

動作確認

実際に関数を実行してみたところ、以下のように「外部アクセスはFailed、かつGoogle Storageのバケット一覧は正常に取得」という狙い通りの結果となりました。

スクリーンショット 2024-11-18 161634

参考

サーバーレスVPCアクセスコネクタとDirect VPC Egress比較

実際にサーバーレスVPCアクセスコネクタとDirect VPC Egressどちらを使えばよいかは以下の公式ドキュメントに比較表があるのでご参照ください。

https://cloud.google.com/run/docs/configuring/connecting-vpc?hl=ja#comparison-table

その他の考慮事項

セキュリティの観点については、ネットワーク経路の制御だけでなく、開発者の操作で外部アクセスを許可しないようにするための「組織ポリシー」による制御も考慮する必要があります。
組織ポリシーをどのように設定すればよいかは以下のブログが参考になると思います。

https://dev.classmethod.jp/articles/organization-policy-setting-forcloud-run-egress/

以上

明日12/03は村田一紘さんです。よろしくお願いします!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.